#ifndef NAHIDABENCHMARKTEST_H
#define NAHIDABENCHMARKTEST_H

#include <functional>
#include <iostream>
#include <fstream>
#include <iomanip>
#include <string>
#include <vector>
#include <chrono>
#include <limits>
#include <cmath>
#include <map>

namespace NahidaProject {
	class __declspec(dllexport) NahidaBenchmarkTest {
	private:
		typedef struct { std::string name; std::vector<double> pct; } Comparison;
		typedef struct { std::string name; std::function<void()> func; } Test;
		typedef struct { std::string name; double minimalRecordedRuntime; double maximalRecordedRuntime; double averageRuntime; double runtimeVariance; double standardDeviation; std::map<long long, size_t> distributionOfRuntimes; } BenchmarkTestResult;

		typedef std::multimap<std::chrono::high_resolution_clock::duration, BenchmarkTestResult> RuntimeResult;
		typedef std::multimap<std::chrono::high_resolution_clock::duration, Comparison> CompareResult;

	public:
		static RuntimeResult Time(int count, std::vector<NahidaBenchmarkTest::Test> func);
		static CompareResult Compare(const RuntimeResult& runtimes);

		template<typename T = std::chrono::microseconds> static void PrintResult(const RuntimeResult& result, int prec = 2, size_t w = 10) {
			for (const auto& a : result) {
				w = a.second.name.size() > w ? a.second.name.size() + 1 : w;
			}
			std::cout << std::setw(w) << " " << std::setw(w) << std::right << "runtime" << std::setw(w) << "min" << std::setw(w) << "max" << std::setw(w) << "avg" << std::setw(w) << "var" << std::setw(w) << "dev" << std::endl;
			double conv = std::chrono::high_resolution_clock::period::den / T::period::den;

			for (const auto& a : result) {
				std::cout << std::setw(w) << std::right << a.second.name << std::setw(w) << std::chrono::duration_cast<T>(a.first).count() << std::setw(w) << std::fixed << std::setprecision(prec) << (a.second.minimalRecordedRuntime / conv) << std::setw(w) << std::fixed << std::setprecision(prec) << (a.second.maximalRecordedRuntime / conv) << std::setw(w) << std::fixed << std::setprecision(prec) << (a.second.averageRuntime / conv) << std::setw(w) << std::fixed << std::setprecision(prec) << (a.second.runtimeVariance / (conv * conv)) << std::setw(w) << std::fixed << std::setprecision(prec) << (a.second.standardDeviation / conv) << std::endl;
			}
		}

		template<typename T = std::chrono::microseconds> static bool WriteResult(const std::string& base, const RuntimeResult& result, char delim = '\t', int prec = 5) {
			double conv = std::chrono::high_resolution_clock::period::den / T::period::den;
			for (const auto& a : result) {
				std::ofstream res(base + "-" + a.second.name + ".txt");
				res << std::chrono::duration_cast<T>(a.first).count() << delim << std::fixed << std::setprecision(prec) << (a.second.minimalRecordedRuntime / conv) << delim << std::fixed << std::setprecision(prec) << (a.second.maximalRecordedRuntime / conv) << delim << std::fixed << std::setprecision(prec) << (a.second.averageRuntime / conv) << delim << std::fixed << std::setprecision(prec) << (a.second.runtimeVariance / conv) << delim << std::fixed << std::setprecision(prec) << (a.second.standardDeviation / conv) << std::endl;
				if (!res.good()) return false;
				std::ofstream dist(base + "-" + a.second.name + "-dist.txt");
				for (const auto& s : a.second.distributionOfRuntimes) {
					dist << s.first << delim << s.second << std::endl;
					if (!dist.good()) return false;
				}
			}
			return true;
		}

		template<typename T = std::chrono::microseconds> static void PrintResult(const CompareResult& compare) {
			size_t w = 10;
			for (const auto& a : compare)
				w = a.second.name.size() > w ? a.second.name.size() + 1 : w;

			std::cout << std::setw(w) << " " << std::setw(w) << std::right << "runtime";
			for (const auto& a : compare)
				std::cout << std::setw(w) << std::right << a.second.name;
			std::cout << std::endl;

			for (const auto& a : compare) {
				std::cout << std::setw(w) << std::right << a.second.name << std::setw(w) << std::chrono::duration_cast<T>(a.first).count();
				for (const auto& p : a.second.pct) {
					if (p == 0) {
						std::cout << std::setw(w) << "--";
						continue;
					}
					std::cout << std::setw(w - 1) << std::fixed << std::setprecision(2) << p << "%" << std::setprecision(0);
				}
				std::cout << std::endl;
			}
		}

	};
}
#endif